/**vue1.0 源码 */

/**
 * 依赖收集
 * 1.1添加依赖（addWatcher）方法，传入来自Observer.get()给的参数'Dep.target'（）实际是在watch（）类中
 *    将当前依赖this 设置到Dep.target中
 * 1.2 notify()(通知方法)，触发依赖数据更新
 */
class Dep{
    constructor(){
        this.dep = []
    }

    //收集依赖（watcher)方法
    addWatcher(watcher){//参数'Dep.target'
        this.dep.push(watcher)
    }

    //notify(通知),更新时候触发，
    notify(){
        this.dep.forEach( watcher => watcher.update())
    }
}

/**
 * xxx
 * 1.1 getOldVal()方法，获取旧的值，同时Dep设置属性target=this(this-->当前的这个依赖)
 *     调用util类的getValue()（？获取即将被修改的数据值？）一次读取数据操作。 同时将Dep.target = null，避免给下次使用受到影响。
 * 
 * 1.2 updata()方法，调用util类的getValue,获取的当前被修改的数据的值，一次读取数据操作。并将数据传入到callb回到，触发回调。
 */
class Watcher{
    constructor(expr, vm, callb){
        this.expr = expr
        this.vm = vm
        this.callb = callb
        this.oldVal = this.getOldVal()
    }

    //获取oldVal,同时收集依赖（watcher）
    getOldVal(){
        //this当前收集的这个依赖(watcher)
        Dep.target = this
        //util.getValue(this.expr, this.vm)
        const oldVal = util.getValue(this.expr, this.vm)
        //清除Dep.target,为了不影响下一次使用这个属性
        Dep.target = null

        return oldVal
    }

    //数据更新时触发
    update(){
        const newVal = util.getValue(this.expr, this.vm)
        if(newVal != this.oldVal){
            this.callb(newVal)
        }
    }
}



/**
 *  ***方法对象，存放处理指令方法***
 *  1.1 属性方法getValue(),获取实例data 的数据(也是getter操作，触发读取操作—-【收集依赖】)
 * 
 *  1.2 属性方法setVal()设置新值（将新值设置到data.expr中）（会触发setter-->触发notify()方法）
 * 
 *  1.3 属性方法model() --> 调用this.getValue(value,vm)读数据，触发getter—-【收集依赖】达到数据绑定目的
 *     1.3.1  new Watcher() 传递参数（value-->vm.data.value(即是{{value}}绑定的数据 ), vm实例对象，回调函数），回调函数触发this.updater.modelUpdate(node, newVal)
 *            进行数据更新，同事触发setter（深度监听，触发订阅发布）。
 *     1.3.2 node.addEventListener（），在DOM对象（即node）添加input事件触发 第1.2步属性方法setVal()设置新值（将新值设置到data.expr中）
 *           （会触发setter-->触发notify()方法）
 *     1.3.3 //初始化数据,同步显示数据【input框内的数据】 this.updater.modelUpdate(node,oldVal)
 *  1.4 on()方法(参数：实例中定义的事件名称，自定义函数名)
 *      获取实例methods上的事件，调用addEventListener，将事件添加到实例中
 *  1.5 text()处理文本绑定
 *  1.6 getContent()获取{{}}内容
 *  1.7 updater()更新DOM的内容--->{{text}}的内容
 *      1.7.1t extUpdate()方法更新文本
 *      1.7.2  modelUpdate()方法 model数据绑定
 */

util = {
    //获取$data 数数据，初始化触发这个函数
    getValue(expr, vm){
        //相当vm.$data.text，得到'hello world'
        //也是一次读物数组操作【getter操作被触发】
        return vm.$data[expr]
    },

    //设置新值
    setVal(expr, vm, newVal){
        //这步操作，会触发setter-->触发notify()方法
        vm.$data[expr] = newVal
    },

    //数据双向绑定
    model(node, value, vm){// <input type=​"text"> , ​ text ,  Vue {$el: '#app', $data: {…}, $options: {…}}
        console.log('model:',node, value, vm)
        console.log(value)
        //完成数据绑定
        const oldVal = this.getValue(value,vm)

        //创建实例，
        new Watcher(value,vm, (newVal)=>{//参数value是vm.data.value(即是{{value}}绑定的数据 )
            console.log('+++++++++++++++++',value,vm)
            console.log('=====--',newVal)
            this.updater.modelUpdate(node, newVal)
        })

        //监听input事件，触发setter
        node.addEventListener('input',(e)=>{
            this.setVal(value, vm, e.target.value)
        })
        //初始化数据,同步显示数据【input框内的数据】
        this.updater.modelUpdate(node,oldVal)
    },

    //绑定事件
    on(node, value, vm, detail){
        //得到实例里method中定义的函数
        const fn = vm.$options.methods[value]
        //事件绑定---detail事件名称，绑定到实例上
        node.addEventListener(detail,fn.bind(vm),false)
    },

    //处理文本绑定
    text(node, content, vm){
        let value;
        
        if(content.includes('{{')){
            //存在{{}}语法
            value = content.replace(/\{\{(.+)\}\}/g, (...args)=>{
                new Watcher(args[1], vm, ( newVal )=>{
                    // this.updater.textUpdate(node,this.getContent(content, vm))
                    this.updater.textUpdate(node, newVal)
                })
            
                //获取$data 数数据，初始化触发这个函数
                return this.getValue(args[1], vm)
            })
            
        }else{
            //获取$data 数数据，初始化触发这个函数
            value = this.getValue(content, vm)
        }

        //更新DOM的内容--->{{text}}的内容
        this.updater.textUpdate(node,value)
    },

    //获取{{}}内容
    getContent(content, vm){
        return content.replace(/\{\{(.+)\}\}/g, (...args)=>{
            return this.getValue(args[1], vm)
        })
    },

    //更新DOM的内容--->{{text}}的内容   
    updater:{
        //更新文本
        textUpdate(node,value){
           node.textContent = value 
        },
        //model数据绑定
        modelUpdate(node, newVal){
            // console.log(node) 触发setter
            node.value = newVal
        }
    },
}


/**数据劫持
/*1.1遍历data中的数据，深度遍历到每个属性，用Object.defineProperty()监听到每个属性读写方法
   1.2 在读取方法中调用Dep类方法收集依赖；
   1.3 在写方法中如果写入的是一个对象，继续遍历对象得到属性进行监听
       并触发Dep 的notify（）方法，触发依赖（watcher）的数据更新
*/

class Observer{
   constructor(data){
    this.observe(data)
   }

   //遍历递归，深度设置属性（gtter,setter）
   observe(data){
      if(data && typeof data === 'object'){

        //Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组
        Object.keys(data).forEach(key =>{
            this.defineReactive(data,key,data[key])//data对象， key属性， data[key]值
        })

      }
   }

   //做数据劫持，监听属性和属性内对象
   defineReactive(obj,key,value){
    //如果value是一个对象，继续递归处理内部key的响应式
    this.observe(value)

    //创建一个数组，把有过访问$data数据的内容都收集起来
    const dep = new Dep()

    Object.defineProperty(obj,key,{
        get(){
            if(Dep.target){//判断是否
                //收集的依赖方法
                dep.addWatcher(Dep.target)
            }
           
            return value
        },
        set:(newVal)=>{
            if(newVal === value) return;
            //如果新设置的value 是一个对象，继续递归处理内部key的响应式
            this.observe(newVal)
            value = newVal
            dep.notify()
        }
    })
   }

}

/**
 * 编译器
 * 
 */
class Compiler{
    constructor(el, vm){
        
        //获取Dom根节点，判断是否是元素节点，并获取
        this.el =this.isElement(el) ? el : document.querySelector(el)
        this.vm = vm

        //创建文档碎片，优化整体更新性能
        const fragment = this.createFragment(this.el)

        //处理文档碎片中，node节点中{{}}、v-、@等语法发内容，数据绑定
        this.compile(fragment)

        //将文档碎片 插入到el中
        this.el.appendChild(fragment)
    }

    //处理文档碎片中，node节点中{{}}、v-、@等语法发内容，数据绑定
    compile(fragment){
        
        //fragment.childNodes 是类数组元素，需转换为真实的数组
        const childNodes = Array.from(fragment.childNodes)
        childNodes.forEach( child =>{
            if(this.isElement(child)){
                //元素标签节点<h1/> <input/>需要处理标签上的属性 v- 、@

                //处理标签节点方法
                this.compileElement(child)
            }else{
                //文本节点{{}} 语法处理
                //处理文本节点方法
                this.compileText(child)
            }

            //DFS 深度优先遍历处理所有子元素
            if(child.childNodes && child.childNodes.length){
                //当前child 有子元素（递归）【注意递归爆栈】
                this.compile(child)
            }
            
        })
    }

    //处理标签节点方法
    compileElement(node){
        //node.attributes 属性返回该元素所有属性节点的一个实时集合是字符串形式的名/值对
        //node.attributes 类数组元素，需转换为真实的数组
        const attributes = Array.from(node.attributes);

        attributes.forEach(attribute =>{
            const {name , value} = attribute
            //处理开头为'v-'方法
            if(this.isDirector(name)){
                //指令 v-
               
                const [,directive] = name.split('-')
                // console.log('directive:',directive)
                //有可能有 v-on:click  的形式
                console.log(directive.split(':'))
                const [key, detail] = directive.split(':')
                console.log('key:',key, 'detail:',detail)

                // console.log('指令',directive, key,detail,value)

                util[key](node, value, this.vm, detail)
                //删除属性，避免而二次绑定
                node.removeAttribute(`v-${directive}`)

            }else if(this.isEventName(name)){//处理开头为'@'方法
                //方法@
                
            }
        })
    }

    //'v-'开头
    isDirector(name){
        //es6新api str.starts() 判断开头
        return name.startsWith('v-')
    }

    //‘@’开头
    isEventName(name){
        //es6新api str.starts() 判断开头
        return name.startsWith('@')
    }

    //处理文本节点方法
    compileText(child){
        //文本节点，处理{{}}
        //获取当前节点的内容node.testContent【textContent 属性表示一个节点及其后代的文本内容。】
        const content = child.textContent
        //正则匹配{{}}的内容
        if(/\{\{(.+)\}\}/.test(content)){
            //调用util,传入文本‘text’
            util['text'](child, content,this.vm)
        }
    }
    
    //创建文档碎片
    createFragment(el){
        // console.dir(el)
        //创建文档碎片，把el的内容 挪到文档碎片中去，后续如果继续使用DOM更新，不会触发真正的 UI 更新，提升性能
        const f = document.createDocumentFragment()
        let firstChild;
        //appendChild 之后，firstChild 自动会被移除
        while(firstChild = el.firstChild){
            // console.log('firstChild----',firstChild)
            f.appendChild(firstChild)
        }
        return f 
    }

    isElement(el){
        //Node.nodeType === 1 表示一个 元素 节点，例如 <p> 和 <div>
        return el.nodeType === 1
    }
}

/**
 * 创建Vue构建函数
 *   处理new Vue({options})传入的实例参数对象
 */
class Vue{
   constructor(options){
     this.$el = options.el;
     this.$data = options.data;
     this.$options = options;

     if(this.$el){
        //1.模板编译
        //2.数据劫持
        
        //观察者（数据劫持处理）
        new Observer(this.$data)

        //模板编译处理
        new Compiler(this.$el, this)

        //将实例上的data中的属性挂在实例this上面，得以this.xxx方式访问属性
        this.proxyData(this.$data)
     }
   }
   
   //将实例上的data中的属性挂在实例this上面，得以this.xxx方式访问属性
   proxyData(data){
    //把this.xx 转发到 this.$data.xx上去
    Object.keys(data).forEach(key =>{
        //将实例data里面的属性挂到实例（vm)this上
        Object.defineProperty(this,key,{
            get(){
                return data[key]
            },
            set(newVal){
                data[key]=newVal
            }
        })
    })
   }
}